---
author: Stéphane Laurent
date: '2019-06-14'
highlighter: 'pandoc-solarized'
linenums: True
output:
html_document:
highlight: kate
keep_md: False
toc: True
md_document:
preserve_yaml: True
toc: True
variant: markdown
prettify: True
prettifycss: minimal
tags: 'R, shiny, javascript, datatables'
rbloggers: yes
title: 'Useful callbacks for DT (in Shiny)'
---
- [Edit cells on pressing Tab and arrow
keys](#edit-cells-on-pressing-tab-and-arrow-keys)
- [Select rows on click and drag](#select-rows-on-click-and-drag)
- [Getting the selected rows](#getting-the-selected-rows)
- [Edit columns headers](#edit-columns-headers)
- [Child tables](#child-tables)
- [Change row CSS properties on clicking an
icon](#change-row-css-properties-on-clicking-an-icon)
- [Capturing the autofilled cells](#capturing-the-autofilled-cells)
- [Select page with a numeric
input](#select-page-with-a-numeric-input)
Edit cells on pressing Tab and arrow keys
=========================================
This callback allows a more friendly way to edit the cells:
- navigate in the table, press 'Enter' to edit;
- press 'Enter' to validate the edit and stay at the same position;
- if you are editing a cell, then pressing 'Tab' or an arrow key will
trigger the edit of the new cell.
This is done with the help of the `KeyTable` extension.
``` {.r}
library(shiny)
library(DT)
js <- c(
"table.on('key', function(e, datatable, key, cell, originalEvent){",
" var targetName = originalEvent.target.localName;",
" if(key == 13 && targetName == 'body'){",
" $(cell.node()).trigger('dblclick.dt');",
" }",
"});",
"table.on('keydown', function(e){",
" var keys = [9,13,37,38,39,40];",
" if(e.target.localName == 'input' && keys.indexOf(e.keyCode) > -1){",
" $(e.target).trigger('blur');",
" }",
"});",
"table.on('key-focus', function(e, datatable, cell, originalEvent){",
" var targetName = originalEvent.target.localName;",
" var type = originalEvent.type;",
" if(type == 'keydown' && targetName == 'input'){",
" if([9,37,38,39,40].indexOf(originalEvent.keyCode) > -1){",
" $(cell.node()).trigger('dblclick.dt');",
" }",
" }",
"});"
)
ui <- fluidPage(
DTOutput("table")
)
server <- function(input, output, session){
output[["table"]] <- renderDT({
datatable(
iris,
selection = "none",
editable = TRUE,
callback = JS(js),
extensions = "KeyTable",
options = list(
keys = TRUE
)
)
})
}
shinyApp(ui, server)
```
![](figures/DTcallback_editOnTab.gif)
Select rows on click and drag
=============================
With this callback, which resorts to `jquery-ui`, you can select some
rows on click and drag. You can also deselect all selected rows by
double-clicking on the table.
``` {.r}
library(shiny)
library(DT)
callback <- c(
"var dt = table.table().node();",
"$(dt).selectable({",
" distance : 10,",
" selecting: function(evt, ui){",
" $(this).find('tbody tr').each(function(i){",
" if($(this).hasClass('ui-selecting')){",
" table.row(i).select();",
" }",
" });",
" }",
"}).on('dblclick', function(){table.rows().deselect();});"
)
ui <- fluidPage(
DTOutput("dt")
)
server <- function(input, output){
output[["dt"]] <- renderDT({
dtable <- datatable(
iris, extensions = "Select",
callback = JS(callback), selection = "multiple"
)
dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
"www/shared/jqueryui",
script = "jquery-ui.min.js",
package = "shiny")
dtable$dependencies <- c(dtable$dependencies, list(dep))
dtable
})
}
shinyApp(ui, server)
```
![](figures/DTcallback_selectOnDrag.gif)
Unfortunately there is an issue: when you sort a column, the selected
rows are lost. Below is another code which overcomes this issue; it uses
a slightly different callback and the option `server = FALSE`.
``` {.r}
library(shiny)
library(DT)
callback <- c(
"var dt = table.table().node();",
"$(dt).selectable({",
" distance : 10,",
" selecting: function(evt, ui){",
" $(this).find('tbody tr').each(function(i){",
" if($(this).hasClass('ui-selecting')){",
" table.row(':eq(' + i + ')').select();",
" }",
" });",
" }",
"}).on('dblclick', function(){table.rows().deselect();});"
)
ui <- fluidPage(
DTOutput("dt")
)
server <- function(input, output){
output[["dt"]] <- renderDT({
dtable <- datatable(
iris, extensions = "Select",
callback = JS(callback), selection = "multiple"
)
dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
"www/shared/jqueryui",
script = "jquery-ui.min.js",
package = "shiny")
dtable$dependencies <- c(dtable$dependencies, list(dep))
dtable
}, server = FALSE)
}
shinyApp(ui, server)
```
### Getting the selected rows
With the above code, `input[["dt_selected_rows"]]` provides only the
rows selected by clicking, not the ones selected by dragging. Here is a
code allowing to get both. The rows selected by clicking are given in
`input[["dt_selected_rows"]]`, while the ones selected by dragging are
given in `input[["dt_selected_rows2"]]`. There are some duplicates so we
have to use `unique`.
``` {.r}
library(shiny)
library(DT)
callback <- c(
"function distinct(value, index, self){
return self.indexOf(value) === index;
}",
"var dt = table.table().node();",
"var tblID = $(dt).closest('.datatables').attr('id');",
"var inputName = tblID + '_rows_selected2'",
"var selected = [];",
"$(dt).selectable({",
" distance : 10,",
" selecting: function(evt, ui){",
" $(this).find('tbody tr').each(function(i){",
" if($(this).hasClass('ui-selecting')){",
" var row = table.row(':eq(' + i + ')')",
" row.select();",
" var rowIndex = parseInt(row.id().split('-')[1]);",
" selected.push(rowIndex);",
" selected = selected.filter(distinct);",
" Shiny.setInputValue(inputName, selected);",
" }",
" });",
" }",
"}).on('dblclick', function(){table.rows().deselect();});",
"table.on('click', 'tr', function(){",
" var row = table.row(this);",
" if(!$(this).hasClass('selected')){",
" var rowIndex = parseInt(row.id().split('-')[1]);",
" var index = selected.indexOf(rowIndex);",
" if(index > -1){",
" selected.splice(index, 1);",
" }",
" }",
" Shiny.setInputValue(inputName, selected);",
"});"
)
ui <- fluidPage(
DTOutput("dt"),
br(),
verbatimTextOutput("selectedRows")
)
dat <- iris
dat$ROWID <- paste0("row-", 1:nrow(dat))
rowNames <- TRUE # whether to show row names in the table
colIndex <- as.integer(rowNames)
server <- function(input, output){
output[["dt"]] <- renderDT({
dtable <- datatable(
dat, rownames = rowNames,
extensions = "Select",
callback = JS(callback),
selection = "multiple",
options = list(
rowId = JS(sprintf("function(data){return data[%d];}",
ncol(dat)-1L+colIndex)),
columnDefs = list( # hide the ROWID column
list(visible = FALSE, targets = ncol(dat)-1L+colIndex)
)
)
)
dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
"www/shared/jqueryui",
script = "jquery-ui.min.js",
package = "shiny")
dtable$dependencies <- c(dtable$dependencies, list(dep))
dtable
}, server = FALSE)
selectedRows <- reactive({
unique(
c(input[["dt_rows_selected"]], input[["dt_rows_selected2"]])
)
})
output[["selectedRows"]] <- renderText({
selectedRows()
})
}
shinyApp(ui, server)
```
Edit columns headers
====================
This callback uses the `jQuery contextMenu` library. It allows to edit a
column header by right-clicking on it. When done, press 'Escape' or move
the mouse.
``` {.r}
library(shiny)
library(DT)
callback <- c(
"$.contextMenu({",
" selector: '#table th',",
" trigger: 'right',",
" autoHide: true,",
" items: {",
" text: {",
" name: 'Enter column header:',",
" type: 'text',",
" value: ''",
" }",
" },",
" events: {",
" show: function(opts){",
" $.contextMenu.setInputValues(opts, {text: opts.$trigger.text()});",
" },",
" hide: function(opts){",
" var $this = this;",
" var data = $.contextMenu.getInputValues(opts, $this.data());",
" var $th = opts.$trigger;",
" $th.text(data.text);",
" }",
" }",
"});"
)
ui <- fluidPage(
tags$head(
tags$link(
rel = "stylesheet",
href = "https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.contextMenu.min.css"
),
tags$script(
src = "https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.contextMenu.min.js"
)
),
DTOutput("table")
)
server <- function(input, output){
output[["table"]] <- renderDT({
datatable(iris, callback = JS(callback))
}, server = FALSE)
}
shinyApp(ui, server)
```
![](figures/DTcallback_editHeaders.png)
Child tables
============
This callback allows to display child tables in the table. The indices
of the selected rows of the child tables are sent to the Shiny server.
``` {.r}
library(shiny)
library(DT)
library(jsonlite)
## data ####
dat <- data.frame(
Sr = c(1.5, 2.3),
Description = c("A - B", "X - Y")
)
## details of row 1
subdat1 <- data.frame(
Chromosome = "chr18",
SNP = "rs2",
stringsAsFactors = FALSE
)
## details of row 2
subdat2 <- data.frame(
Chromosome = c("chr19","chr20"),
SNP = c("rs3","rs4"),
stringsAsFactors = FALSE
)
## merge the row details
subdats <- lapply(list(subdat1, subdat2), purrr::transpose)
## dataframe for the datatable
Dat <- cbind(" " = "expand", dat, details = I(subdats))
## the callback ####
registerInputHandler("x.child", function(x, ...) {
fromJSON(toJSON(x, auto_unbox = TRUE, null = "null"),
simplifyDataFrame = FALSE)
}, force = TRUE)
callback = JS(
"var expandColumn = table.column(0).data()[0] === 'plus-sign' ? 0 : 1;",
"table.column(expandColumn).nodes().to$().css({cursor: 'pointer'});",
"",
"// send selected columns of the main table to Shiny",
"var tbl = table.table().node();",
"var tblId = $(tbl).closest('.datatables').attr('id');",
"var selector = 'td:not(:nth-child(' + (expandColumn+1) + '))';",
"table.on('click', selector, function(){",
" setTimeout(function(){",
" var indexes = table.rows({selected:true}).indexes();",
" var indices = Array(indexes.length);",
" for(var i = 0; i < indices.length; ++i){",
" indices[i] = indexes[i];",
" }",
" Shiny.setInputValue(tblId + '_rows_selected', indices);",
" },0);",
"});",
"",
"// make the table header of the nested table",
"var format = function(d, childId){",
" if(d != null){",
" var html = '
';",
" for(var key in d[d.length-1][0]){",
" html += '' + key + ' | ';",
" }",
" html += '
'",
" return html;",
" } else {",
" return '';",
" }",
"};",
"",
"// row callback to style the rows background colors of the child tables",
"var rowCallback = function(row, dat, displayNum, index){",
" if($(row).hasClass('odd')){",
" $(row).css('background-color', 'papayawhip');",
" $(row).hover(function(){",
" $(this).css('background-color', '#E6FF99');",
" }, function() {",
" $(this).css('background-color', 'papayawhip');",
" });",
" } else {",
" $(row).css('background-color', 'lemonchiffon');",
" $(row).hover(function(){",
" $(this).css('background-color', '#DDFF75');",
" }, function() {",
" $(this).css('background-color', 'lemonchiffon');",
" });",
" }",
"};",
"",
"// header callback to style the header of the child tables",
"var headerCallback = function(thead, data, start, end, display){",
" $('th', thead).css({",
" 'border-top': '3px solid indigo',",
" 'color': 'indigo',",
" 'background-color': '#fadadd'",
" });",
"};",
"",
"// make the child table",
"var format_datatable = function(d, childId){",
" var dataset = [];",
" var n = d.length - 1;",
" for(var i = 0; i < d[n].length; i++){",
" var datarow = $.map(d[n][i], function(value, index){",
" return [value];",
" });",
" dataset.push(datarow);",
" }",
" var id = 'table#' + childId;",
" var subtable = $(id).DataTable({",
" 'data': dataset,",
" 'autoWidth': true,",
" 'deferRender': true,",
" 'info': false,",
" 'lengthChange': false,",
" 'ordering': d[n].length > 1,",
" 'order': [],",
" 'paging': false,",
" 'scrollX': false,",
" 'scrollY': false,",
" 'searching': false,",
" 'sortClasses': false,",
" 'rowCallback': rowCallback,",
" 'headerCallback': headerCallback,",
" 'select': {style: 'multi'},",
" 'columnDefs': [{targets: '_all', className: 'dt-center'}]",
" });",
"};",
"",
"// send selected rows of the children tables to shiny server",
"var nrows = table.rows().count();",
"var nullinfo = Array(nrows);",
"for(var i = 0; i < nrows; ++i){",
" nullinfo[i] = {row: i, selected: null};",
"}",
"Shiny.setInputValue(tblId + '_children:x.child', nullinfo);",
"var sendToR = function(){",
" var info = [];",
" setTimeout(function(){",
" for(var i = 0; i < nrows; ++i){",
" var childId = 'child-' + i;",
" var childtbl = $('#'+childId).DataTable();",
" var indexes = childtbl.rows({selected:true}).indexes();",
" var indices;",
" if(indexes.length > 0){",
" indices = Array(indexes.length);",
" for(var j = 0; j < indices.length; ++j){",
" indices[j] = indexes[j];",
" }",
" } else {",
" indices = null;",
" }",
" info.push({row: i, selected: indices});",
" }",
" Shiny.setInputValue(tblId + '_children:x.child', info);",
" }, 0);",
"}",
"$('body').on('click', '[id^=child-] td', sendToR);",
"",
"// click event to show/hide the child tables",
"table.on('click', 'td.details-control', function () {",
" var cell = table.cell(this);",
" row = table.row($(this).closest('tr'));",
" if(row.child.isShown()){",
" row.child.hide();",
" cell.data('expand');",
" sendToR();",
" } else {",
" var childId = 'child-' + row.index();",
" row.child(format(row.data(), childId)).show();",
" row.child.show();",
" cell.data('collapse-down');",
" format_datatable(row.data(), childId);",
" }",
"});")
## render function, to display the glyphicons ####
render <- c(
"function(data, type, row, meta){",
" if(type === 'display'){",
" return '' + ",
" '';",
" } else {",
" return data;",
" }",
"}"
)
## shiny app ####
ui <- fluidPage(
DTOutput("table"),
br(),
fluidRow(
column(6,
tags$label("Selected row(s) of main table:"),
verbatimTextOutput("info-main")
),
column(6,
tags$label("Selected row(s) of child tables:"),
verbatimTextOutput("info-children")
)
)
)
server <- function(input, output){
output[["table"]] <- renderDT({
datatable(Dat, callback = callback, escape = -2,
extensions = "Select", selection = "none",
options = list(
select = list(style = "multi", selector = ".selectable"),
autoWidth = FALSE,
columnDefs = list(
list(className = "selectable dt-center",
targets = c(0, 2:ncol(Dat))),
list(visible = FALSE, targets = ncol(Dat)),
list(orderable = FALSE, className = 'details-control',
width = "10px", render = JS(render), targets = 1),
list(className = "dt-center", targets = "_all")
)
)
)
}, server = FALSE)
output[["info-main"]] <- renderText({
capture.output(input[["table_rows_selected"]])
})
output[["info-children"]] <- renderText({
paste0(capture.output(input[["table_children"]]), collapse = "\n")
})
}
shinyApp(ui, server)
```
![](figures/DTcallback_childTables.png)
Change row CSS properties on clicking an icon
=============================================
This callback allows to change the CSS properties of a row by clicking
an icon. The indices of the altered rows are sent to the Shiny server.
``` {.r}
library(shiny)
library(DT)
rowNames <- TRUE # whether to show row names in the table
colIndex <- as.integer(rowNames)
callback <- c(
sprintf("table.on('click', 'td:nth-child(%d)', function(){", colIndex+1),
" var td = this;",
" var cell = table.cell(td);",
" if(cell.data() === 'ok'){",
" cell.data('remove');",
" } else {",
" cell.data('ok');",
" }",
" var $row = $(td).closest('tr');",
" $row.toggleClass('excluded');",
" var excludedRows = [];",
" table.$('tr').each(function(i, row){",
" if($(this).hasClass('excluded')){",
" excludedRows.push(parseInt($(row).attr('id').split('_')[1]));",
" }",
" });",
" Shiny.setInputValue('excludedRows', excludedRows);",
"})"
)
restore <- c(
"function(e, table, node, config) {",
" table.$('tr').removeClass('excluded').each(function(){",
sprintf(" var td = $(this).find('td').eq(%d)[0];", colIndex),
" var cell = table.cell(td);",
" cell.data('ok');",
" });",
" Shiny.setInputValue('excludedRows', null);",
"}"
)
render <- c(
'function(data, type, row, meta){',
' if(type === "display"){',
' var color = data === "ok" ? "forestgreen" : "red";',
' return "";',
' } else {',
' return data;',
' }',
'}'
)
ui <- fluidPage(
tags$head(
tags$style(HTML(
".excluded { color: rgb(211,211,211); font-style: italic; }"
))
),
fluidRow(
column(
6,
tags$label("Excluded rows"),
verbatimTextOutput("excludedRows")
),
column(
6,
tags$label("Included rows"),
verbatimTextOutput("includedRows")
)
),
br(),
DTOutput("mytable")
)
server <- function(input, output,session) {
dat <- cbind(Selected = "ok", mtcars[1:6,], id = paste0("row_",1:6))
output[["mytable"]] <- renderDT({
datatable(dat, rownames = rowNames,
extensions = c("Select", "Buttons"),
selection = "none",
callback = JS(callback),
options = list(
rowId = JS(sprintf("function(data){return data[%d];}",
ncol(dat)-1+colIndex)),
columnDefs = list(
list(visible = FALSE, targets = ncol(dat)-1+colIndex),
list(className = "dt-center", targets = "_all"),
list(className = "notselectable", targets = colIndex),
list(targets = colIndex, render = JS(render))
),
dom = "Bt",
buttons = list("copy", "csv",
list(
extend = "collection",
text = 'Select all rows',
action = JS(restore)
)
),
select = list(style = "single",
selector = "td:not(.notselectable)")
)
)
}, server = FALSE)
output$excludedRows <- renderPrint({
input[["excludedRows"]]
})
output$includedRows <- renderPrint({
setdiff(1:nrow(dat), input[["excludedRows"]])
})
}
shinyApp(ui, server)
```
![](figures/DTcallback_rowCSSonclick.png)
Capturing the autofilled cells
==============================
The `AutoFill` extension gives an Excel like option to a DataTable to
click and drag over multiple cells, filling in information over the
selected cells and incrementing numbers as needed.
The callback below allows to update the data in the R server when some
cells are edited or changed by autofilling.
``` {.r}
library(shiny)
library(DT)
callback <- c(
"var tbl = $(table.table().node());",
"var id = tbl.closest('.datatables').attr('id');",
"table.on('autoFill', function(e, datatable, cells){",
" var out = [];",
" for(var i = 0; i < cells.length; ++i){",
" var cells_i = cells[i];",
" for(var j = 0; j < cells_i.length; ++j){",
" var c = cells_i[j];",
" var value = c.set === null ? '' : c.set;", # null => problem in R
" out.push({",
" row: c.index.row + 1,",
" col: c.index.column,",
" value: value",
" });",
# to color the autofilled cells, uncomment the two lines below
# " $(table.cell(c.index.row, c.index.column).node())",
# " .css('background-color', 'yellow');",
" }",
" }",
" Shiny.setInputValue(id + '_cells_filled:DT.cellInfo', out);",
" table.rows().invalidate();", # this updates the column type
"});"
)
ui <- fluidPage(
br(),
DTOutput("dt"),
br(),
verbatimTextOutput("table")
)
server <- function(input, output){
dat <- iris[1:5,]
dat$Species <- as.character(dat$Species)
output[["dt"]] <- renderDT({
datatable(dat,
editable = list(target = "cell"),
selection = "none",
extensions = "AutoFill",
callback = JS(callback),
options = list(
autoFill = TRUE
)
)
}, server = TRUE)
Data <- reactive({
info <- rbind(input[["dt_cells_filled"]], input[["dt_cell_edit"]])
if(!is.null(info)){
info <- unique(info)
info$value[info$value==""] <- NA
dat <<- editData(dat, info, proxy = "dt")
}
dat
})
output[["table"]] <- renderPrint({Data()})
}
shinyApp(ui, server)
```
![](figures/DTcallback_AutoFill.gif)
If you use `server = FALSE` in `renderDT`, just remove the `proxy`
argument in `editData`:
``` {.r}
dat <<- editData(dat, info)
```
Select page with a numeric input
================================
The default pagination is not convenient when there are many pages (the
user has to click multiple times on the 'Next' or 'Previous' button).
This callback allows to select a page with a numeric input.
``` {.r}
library(shiny)
library(DT)
shinyApp(
ui = fluidPage(
tags$head(tags$style(".pagination {float: right;}")),
fluidRow(
div(id="pagination",
div(style = "display:inline-block;",
tags$a(id = "first", style = "cursor: pointer;", "First")),
div(style = "display:inline-block;",
tags$a(id = "previous", style = "cursor: pointer;", " Previous")),
div(style = "display:inline-block;",
tags$input(id="page", type="number", class="input-sm", value="1", min="1")
),
div(style = "display:inline-block;",
tags$span(id = "of")),
div(style = "display:inline-block;",
tags$a(id = "next", style = "cursor: pointer;", "Next ")),
div(style = "display:inline-block;",
tags$a(id = "last", style = "cursor: pointer;", "Last"))
)
),
fluidRow(
column(12, DTOutput('tbl'))
)
),
server = function(input, output) {
output$tbl = renderDT({
datatable(
iris, options = list(
dom = "lfrti<'pagination'>",
initComplete = JS(c(
"function(settings, json){",
" var table = settings.oInstance.api();",
" var pageinfo = table.page.info();",
" $('#of').text('of ' + pageinfo.pages);",
"}"
))
),
callback = JS(c(
"$('div.pagination').append($('#pagination'));",
"$('#first').on('click', function(){",
" table.page('first').draw('page');",
" $('#page').val(1);",
"});",
"$('#previous').on('click', function(){",
" table.page('previous').draw('page');",
" $('#page').val(table.page.info().page + 1);",
"});",
"$('#next').on('click', function(){",
" table.page('next').draw('page');",
" $('#page').val(table.page.info().page + 1);",
"});",
"$('#last').on('click', function(){",
" table.page('last').draw('page');",
" $('#page').val(table.page.info().pages);",
"});",
"$('#page').on('change', function(){",
" var page = parseInt($('#page').val());",
" if(!isNaN(page)){ table.page(page-1).draw('page'); }",
"});"
))
)
})
}
)
```
![](figures/DTcallback_pagination.gif)